{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "08390475",
   "metadata": {},
   "source": [
    "# Multivariate Time Series Anomaly Detection\n",
    "\n",
    "Multivariate time series anomaly detection works in largely the same way as univariate time series anomaly detection (covered [here](0_AnomalyIntro.ipynb) and [here](1_AnomalyFeatures.ipynb)). To begin, we will load the multivariate `MSL` dataset for time series anomaly detection."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "894a554f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Time series is 55-dimensional\n"
     ]
    }
   ],
   "source": [
    "from merlion.utils import TimeSeries\n",
    "from ts_datasets.anomaly import MSL\n",
    "\n",
    "time_series, metadata = MSL()[0]\n",
    "train_data = TimeSeries.from_pd(time_series[metadata.trainval])\n",
    "test_data = TimeSeries.from_pd(time_series[~metadata.trainval])\n",
    "test_labels = TimeSeries.from_pd(metadata.anomaly[~metadata.trainval])\n",
    "\n",
    "print(f\"Time series is {train_data.dim}-dimensional\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b9174b4",
   "metadata": {},
   "source": [
    "## Model Initialization and Training\n",
    "\n",
    "For the purposes of this tutorial, we will be using 3 models:\n",
    "\n",
    "1. `DefaultDetector` (which automatically detects whether the input time series is univariate or multivariate);\n",
    "2. `IsolationForest` (a classic algorithm); and \n",
    "3. A `DetectorEnsemble` which takes the maximum anomaly score returned by either model.\n",
    "\n",
    "Note that while all multivariate anomaly detection models can be used on univariate time series, some Merlion models (e.g. `WindStats`, `ZMS`, `StatThreshold`) are specific to univariate time series. However, the API is identical to that of univariate anomaly detection models."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "cc5c6e4b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Training DefaultDetector...\n",
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.5998\n",
      "Training IsolationForest...\n",
      "Training DetectorEnsemble...\n",
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.6001\n"
     ]
    }
   ],
   "source": [
    "# We initialize models using the model factory in this tutorial\n",
    "# We manually set the detection threshold to 2 (in standard deviation units) for all models\n",
    "from merlion.models.factory import ModelFactory\n",
    "from merlion.post_process.threshold import AggregateAlarms\n",
    "\n",
    "model1 = ModelFactory.create(\"DefaultDetector\",\n",
    "                             threshold=AggregateAlarms(alm_threshold=2))\n",
    "\n",
    "model2 = ModelFactory.create(\"IsolationForest\",\n",
    "                             threshold=AggregateAlarms(alm_threshold=2))\n",
    "\n",
    "# Here, we create a _max ensemble_ that takes the maximal anomaly score\n",
    "# returned by any individual model (rather than the mean).\n",
    "model3 = ModelFactory.create(\"DetectorEnsemble\", models=[model1, model2],\n",
    "                             threshold=AggregateAlarms(alm_threshold=2),\n",
    "                             combiner={\"name\": \"Max\"})\n",
    "\n",
    "for model in [model1, model2, model3]:\n",
    "    print(f\"Training {type(model).__name__}...\")\n",
    "    train_scores = model.train(train_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15cb56f7",
   "metadata": {},
   "source": [
    "## Model Inference and Quantitative Evaluation\n",
    "\n",
    "Like univariate models, we may call `get_anomaly_label()` to get a sequence of post-processed (calibrated and thresholded) training scores. We can then use these to evaluate the model's performance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e18c0534",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DefaultDetector\n",
      "Precision: 0.9659\n",
      "Recall:    0.8312\n",
      "F1:        0.8935\n",
      "MTTD:      0 days 01:21:15\n",
      "\n",
      "IsolationForest\n",
      "Precision: 0.9638\n",
      "Recall:    0.8192\n",
      "F1:        0.8856\n",
      "MTTD:      0 days 01:40:57\n",
      "\n",
      "DetectorEnsemble\n",
      "Precision: 0.9620\n",
      "Recall:    0.8184\n",
      "F1:        0.8844\n",
      "MTTD:      0 days 01:34:51\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from merlion.evaluate.anomaly import TSADMetric\n",
    "\n",
    "for model in [model1, model2, model3]:\n",
    "    labels = model.get_anomaly_label(test_data)\n",
    "    precision = TSADMetric.PointAdjustedPrecision.value(ground_truth=test_labels, predict=labels)\n",
    "    recall = TSADMetric.PointAdjustedRecall.value(ground_truth=test_labels, predict=labels)\n",
    "    f1 = TSADMetric.PointAdjustedF1.value(ground_truth=test_labels, predict=labels)\n",
    "    mttd = TSADMetric.MeanTimeToDetect.value(ground_truth=test_labels, predict=labels)\n",
    "    print(f\"{type(model).__name__}\")\n",
    "    print(f\"Precision: {precision:.4f}\")\n",
    "    print(f\"Recall:    {recall:.4f}\")\n",
    "    print(f\"F1:        {f1:.4f}\")\n",
    "    print(f\"MTTD:      {mttd}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2972aa3e",
   "metadata": {},
   "source": [
    "We can also use a `TSADEvaluator` to evaluate a model in a manner that simulates live deployment. Here, we train an initial model on the training data, and we obtain its predictions on the training data using a sliding window of 1 week (`cadence=\"1w\"`). However, we only retrain the model every 4 weeks (`retrain_freq=\"4w\"`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "47a4508d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DefaultDetector Sliding Window Evaluation\n",
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.5998\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "TSADEvaluator:  55%|█████▍    | 2419200/4423680 [00:23<00:19, 101454.83it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.6667\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "TSADEvaluator: 100%|██████████| 4423680/4423680 [02:09<00:00, 34157.67it/s] \n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Precision: 0.9522\n",
      "Recall:    0.8027\n",
      "F1:        0.8711\n",
      "MTTD:      0 days 01:22:46\n",
      "\n",
      "IsolationForest Sliding Window Evaluation\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "TSADEvaluator: 100%|██████████| 4423680/4423680 [00:27<00:00, 160149.84it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Precision: 0.9666\n",
      "Recall:    0.8321\n",
      "F1:        0.8943\n",
      "MTTD:      0 days 01:40:42\n",
      "\n",
      "DetectorEnsemble Sliding Window Evaluation\n",
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.6002\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "TSADEvaluator:  55%|█████▍    | 2419200/4423680 [00:28<00:24, 83276.94it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " |████████████████████████████████████████| 100.0% Complete, Loss 0.6532\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "TSADEvaluator: 100%|██████████| 4423680/4423680 [02:37<00:00, 28002.66it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Precision: 0.9453\n",
      "Recall:    0.8209\n",
      "F1:        0.8787\n",
      "MTTD:      0 days 01:32:18\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from merlion.evaluate.anomaly import TSADEvaluator, TSADEvaluatorConfig\n",
    "for model in [model1, model2, model3]:\n",
    "    print(f\"{type(model).__name__} Sliding Window Evaluation\")\n",
    "    evaluator = TSADEvaluator(model=model, config=TSADEvaluatorConfig(\n",
    "        cadence=\"1w\", retrain_freq=\"4w\"))\n",
    "    train_result, test_pred = evaluator.get_predict(train_vals=train_data, test_vals=test_data)\n",
    "    precision = evaluator.evaluate(ground_truth=test_labels, predict=test_pred,\n",
    "                                   metric=TSADMetric.PointAdjustedPrecision)\n",
    "    recall = evaluator.evaluate(ground_truth=test_labels, predict=test_pred,\n",
    "                                metric=TSADMetric.PointAdjustedRecall)\n",
    "    f1 = evaluator.evaluate(ground_truth=test_labels, predict=test_pred,\n",
    "                            metric=TSADMetric.PointAdjustedF1)\n",
    "    mttd = evaluator.evaluate(ground_truth=test_labels, predict=test_pred,\n",
    "                              metric=TSADMetric.MeanTimeToDetect)\n",
    "    print(f\"Precision: {precision:.4f}\")\n",
    "    print(f\"Recall:    {recall:.4f}\")\n",
    "    print(f\"F1:        {f1:.4f}\")\n",
    "    print(f\"MTTD:      {mttd}\")\n",
    "    print()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
